<?php
/**
 * Data 클래스를 위한 파일 
 *
 * @package php.lang
 */

/**
 * 레코드 형태의 Data를 관리해주는 클래스
 *
 * @package php.lang
 */
class Data extends PObject implements ArrayAccess {

	/**
	 * Connector (연결자)
	 *
	 * @var		IConnector
	 */
	private $__connector;

	/**
	 * Data 클래스를 상속한 클래스 이름
	 *
	 * @var		string		
	 */
	private $baseClass;

	/**
	 * data 개수 
	 *
	 * @var		integer	
	 */
	private $listCount;			

	/**
	 * 필드의 이름 배열
	 *
	 * @var		array
	 */
	private $fields = array();

	/**
	 * row 데이타 배열
	 *
	 * @var		array
	 */
	private $row = array();

	/**
	 * row 데이타를 가리키는 index
	 *
	 * @var		int
	 */
	private $index;

	/**
	 * row 데이타를 가리키는 temp index
	 *
	 * @var		int
	 */
	private $tempIndex;
	
	/**
	 * groupBy 할때 구분자로 쓰이는 문자열 
	 * 
	 * @var string
	 */
	const KEY_FIELD = "%%key_field%%";

	/**
	 * 생성자
	 *
	 * @param	IConnector $connector	연결자 
	 */
	public function __construct($connector = null, $fields = array(), $rows = array())
	{
		$this->listCount = 0;

		$this->setIndex(-1);
		$this->setConnector($connector);

		if ($fields)	$this->setFields($fields);
		if ($rows)		$this->setRows($rows);
	}

	/**
	 * 연결자 설정
	 *
	 * @param	IConnector $connector	연결자 
	 */
	public function setConnector($connector) {
		$this->__connector = $connector;
	}

	/**
	 * 연결자 구하기 
	 *
	 * @return IConnector  연결자 
	 */
	public function getConnector() { 
		return $this->__connector;
	}

	/**
	 * 리스트 개수가 0 인지 체크 
	 *
	 * @return	bool		true : 비어 있음, false : 데이타가 1개 이상 
	 */
	public function isEmpty(){
		return ($this->listCount == 0);
	}

	/**
	 * 다음 레코드로 이전, 없으면 false를 리턴
	 *
	 * @return	bool		true : 데이타 있음, false : 다음 데이타가 없음 
	 */
	public function next()
	{
		if ($this->index >= $this->listCount-1) {
			$this->index = $this->listCount;	
			return false;
		}

		$this->index++;

		return true;
	}

	/**
	 * 이전 레코드로 이전 , 없으면 false를 리턴 
	 *
	 * @return	bool		true : 이전 데이타 있음, false : 이전 데이타가 없음 
	 */
	public function prev()
	{
		if ($this->index > -1) {
			$this->index--;
			
			return true;
		}

		return false;
	}

	/**
	 * 인덱스 초기화.  -1로 초기화
	 *
	 */
	public function init()
	{
		$this->setIndex(-1);
	}

	/**
	 * 현재 인덱스 구하기 
	 *
	 * @return	int  현재 인덱스 번호 
	 */
	public function getIndex()
	{
		return $this->index;
	}

	/**
	 * 현재 인덱스 설정하기
	 *
	 * @param	int $index 설정될 인덱스 값 
	 * @return	Data  현재 Data 객체
	 */
	public function setIndex($index)
	{
		$this->index = $index;
		return $this;
	}
	
	/**
	 * 현재의 인덱스를 임시 인덱스 번호에 저장하고, 인덱스를 초기화한다. 
	 *
	 */
	public function saveIndex()
	{
		$this->tempIndex = $this->getIndex();
		$this->init();
	}

	/**
	 * 임시 인덱스 복원
	 *
	 */
	public function returnIndex()
	{
		$this->setIndex($this->tempIndex);
	}


	/**
	 * 필드값을 얻어온다.
	 * 
	 * 
	 * get() 메소드와 같은 메소드
	 * 다른점은 객체의 속성처럼 사용 할 수 있게 만들어준다. (php5 이상만 제공된다.) 
	 * 
	 * <code>
	 *  $data = new Data(); 
	 *  while($data->next()) { 
	 *     echo $data->name;
	 *  }
	 * </code>
	 *
	 * 위와 같은 코드는 name 이라는 public 속성이 없기 때문에 __get 메소드를 호출한다. 
	 * __get 메소드는 다시 get 메소드를 호출하고 일련의 필드 값에 접근 가능하다.
	 * 
	 *
	 * @param	string $key	검색될 필드값, 대소문자 구분 없음 
	 * @return	mixed
	 */
	public function __get($key)
	{
		return $this->get($key);
	}

	/**
	 * 필드 값을 설정한다. set 메소드와 같은 역할 
	 *
	 * @param	string $key	검색될 필드값, 대소문자 구분 없음 
	 * @param	mixed $value 설정될 값
	 */
	public function __set($key, $value)
	{
		$this->set($key, $value);
	}
	
	/**
	 * utility 형태 메소드 동적 실행 
	 * 
	 * is : 값 체크 메소드 
	 * 
	 * <code> echo $data->isY('field');  // true or false </code>
	 * 
	 * @param string $method 메소드 이름 
	 * @param array $args 매개변수 
	 * @return mixed
	 */
	public function __call($method, $args = array()) { 
		
		// is 체크 메소드  (문자열만 체크 가능 ) 
		if (strpos($method, "is") === 0) { 
			$temp = explode("is", $method);
			array_shift($temp);
			$str = implode("is", $temp);
			
			return ($str == $this->get($args[0]));
		}
		
		
	}

	/**
	 * 현재 인덱스에 해당하는 row의 필드 값을 가지고 온다. 
	 *
	 * <code>
	 * $data->get("field");	// 필드 이름을 지정한다. 
	 * or
	 * $data->get(0);		// 필드 순서를 지정할 수 있다. 
	 * </code>
	 *
	 * @param	string|int $key	$key가 문자열일 경우 검색될 필드이름, 필드이름은 대소문자를 구분하지 않는다. 
	 * @param	mixed $default 필드가 없거나 필드에 값이 없을때 $default 값을 돌려준다. 
	 * @param	array $option 기타 옵션들 
	 * @return	mixed
	 */
	public function get($key, $default = '', $option = array())
	{

		if ($this->index == -1 || $this->index == $this->listCount) {
			return $default;
		}

		// key가 int 형일때는 field 명으로 변경한다. 
		if (is_int($key)) { 
			$key = $this->fields[$key];
		}

		$key = $this->filterKey($key);
		$data = $this->row[$this->index][$key];

		if (is_null($data)) return $data;
		if (is_numeric($data)) return $data;

		return ($data) ? $data : $default;
	}

	/**
	 * 현재 인덱스에 해당하는 row의 필드 값을 설정한다. 
	 *
	 * <code>
	 * $data->set("field", "value");	// 필드 이름을 지정한다. 
	 * or
	 * $data->get(0, "value");			// 필드 순서를 지정할 수 있다. 
	 * </code>
	 *
	 * @param	string|int $key		검색될 필드이름, 필드이름은 대소문자를 구분하지 않는다. 
	 * @param	mixed $value	설정할 필드 값
	 */
	public function set($key, $value)
	{
		// key가 int 형일때는 field 명으로 변경한다. 
		if (is_int($key)) { 
			$key = $this->fields[$key];
		}

		$key = $this->filterKey($key);
		$this->row[$this->index][$key] = $value;
	}

	/**
	 * 현재 인덱스에 해당하는 row의 값을 얻어온다.
	 *
	 * @return	array
	 */
	public function gets()
	{
		return $this->getRow($this->index);
	}

	/**
	 * 처음 row의 값을 얻어온다.
	 *
	 * @return	array  한 행의 배열
	 */
	public function getFirst() { 
		return $this->getRow(0);
	}

	/**
	 * 마지막 row의 값을 얻어온다.
	 *
	 * @return	array 한 행의 배열
	 */
	public function getLast() { 
		return $this->getRow($this->listCount-1);
	}

	/**
	 * 인덱스 위치를 처음으로 이동
	 *
	 */
	public function moveFirst() { 
		$this->setIndex(0);
	}

	/**
	 * 인덱스 위치를 마지막으로 이동
	 *
	 */
	public function moveLast() { 
		$this->setIndex($this->getListCount() - 1);
	}

	/**
	 * 인덱스 위치가 처음인지 얻어옴 
	 *
	 * @return 	bool  처음이면 true, 아니면 false  
	 */
	public function isFirst() { 
		return ($this->getIndex() == 0);
	}

	/**
	 * 인덱스 위치가 마지막인지 얻어옴 
	 *
	 * @return 	bool 마지막이면 true, 아니면 false  
	 */
	public function isLast() { 
		return ($this->getListCount()-1 == $this->getIndex());
	}

	/**
	 * 인덱스 위치가 초기화 시점인지 얻어옴 
	 *
	 * @return 	bool 인덱스가 초기시점 true, 아니면 false  
	 */
	public function isInit() { 
		return ($this->getIndex() == -1);
	}

	/**
	 * 필드이름 필터링
	 *
	 * @param	string $key	변환될 필드 이름 
	 * @return	string 대문자로 변환된 키이름
	 */
	protected function filterKey($key) {
		return strtolower($key);
	}
	
	/**
	 * 해당 인덱스의 row 값 리턴
	 *
	 * @param	int $index 지정된 인덱스 값
	 * @return	array  행 배열 
	 */
	public function getRow($index){
		return $this->row[$index];
	}

	/**
	 * data가 가지고 있는 모든 row 값을 리턴한다. 
	 *
	 * @return	array  2차원 전체 배열
	 */
	public function getRows()
	{
		return $this->row;
	}


	/** 
	 * 특정 함수를 이용해서 변환된 값을 저장한다 
	 *
	 * <code>
	 *  echo $data->loop(array('func' => 'strtoupper', 'field' => '필드1,필드2'), ...);
	 * </code>
	 * 
	 * 구현예정 
	 * <code>
	 * echo $data->loop(array( 'func' => 'strtoupper', 'param' => array( array('value', '값'),  array('field', '필드1,필드2'))), ...);
	 * </code>
	 * 
	 * @param array ...  실행될 함수 배열  
	 * @deprecated
	 */
	public function loop() { 
		$arg_list = func_get_args();

		if (count($arg_list) == 0) 
			return $this;

		$this->saveIndex();

		while($this->next()) { 

			foreach ($arg_list as $arg)  { 
				$arg['field'] = array_map('trim', explode(",", $arg['field']));

				foreach ($arg['field'] as $field) { 
					$this->{$field} = call_user_func_array($arg['func'], array($this->{$field}));
				}
			}
		}

		$this->returnIndex();


		return $this;
	}

	/**
	 * data가 가지고 있는 모든 row 값을 설정한다.
	 *
	 * @param	array $rows		저장될 데이타 값 배열
	 * @param	bool $is_add	false 이면 이전 데이타 변경, true 이면 기존 데이타에 추가
	 * @return Data  
	 */
	public function setRows(array $rows, $is_add = false)
	{
		if ($is_add) { 
			$this->appendRows($rows);
		} else  { 
			$this->row = array_merge($rows);
			$this->listCount = count($this->row);
		}
		
		return $this;
	}
	
	/**
	 * 여러개의 row 추가하기 
	 * 
	 * @param array $rows
	 * @return Data
	 */
	public function appendRows(array $rows) { 
		foreach ($rows as $row) { 
			$this->add($row);
		}		
		
		return $this;
	}

	/**
	 * row 데이타 추가하기 
	 *
	 * @param	array $arr 저장될 데이타 값 배열
	 * @return  Data  현재 Data 객체
	 */
	public function add(array $arr)
	{
		$index = $this->listCount;

		// 임시 인덱스 설정
		$this->saveIndex();

		// 빈 행 삽입 
		$this->row[$index] = array(); 

		// 인덱스 설정 하고 
		$this->setIndex($index);

		// 바인딩 하면 잘 들어가네..
		$arr = $this->map($arr);
		foreach ($this->fields as $field) {
			$this->set($field, $arr[$field]);
		}

		// 리스트 카운트 증가 
		$this->listCount++;

		// 원래의 인덱스로 돌리기 
		$this->returnIndex();

		return $this;
	}

	/**
	 * row 데이타 바인딩
	 *
	 * @param	array $arr	저장될 데이타 값 배열
	 * @param	array $omit	바인딩 되지 않을 값 배열 
	 * @return  Data 현재 Data 객체
	 */
	public function bind($arr, $omit = array('num')) {
		$arr = $this->map($arr);
		$omit = array_map('strtolower', $omit);

		$keys = array_keys($arr);

		foreach ($this->fields as $field) {
			
			// 필드 값 중에 arr의 키값으로 없으면 바인딩을 하지 않는다. 
			if (!in_array($field, $keys)) { 
				continue;
			}

			// 바인딩 필드에서 제외
			if (in_array($field, $omit)) { 
				continue;
			}

			if ($arr[$field]) { 
				$this->set($field, $arr[$field]);
			}
		}

		return $this;
	}

	/**
	 * row 데이타 바인딩
	 *
	 * @param	array $arr	저장될 데이타 값 배열
	 * @return  Data 현재 Data 객체
	 */
	public function directBind($arr) {
		foreach ($arr as $field => $value) {
			if ( in_array($field, $this->fields)) { 
				$this->{$field} = $value;
			}
		}

		return $this;
	}

	/**
	 * row에 저장될 데이타를 키값을 동일하게 소문자로 바꿔준다.
	 *
	 * @param	array $arr	저장될 데이타 값 배열
	 * @return	array	
	 */
	protected function map(array $arr) {
		return array_change_key_case($arr, CASE_LOWER);
	}

	/** 
	 * 필드 이름 바꾸기 
	 *
	 * <code>$data->convertField(array('A' => '이전', 'B' => '이후'))->display();</code>
	 *
	 * @param	array $field_list	바뀔 필드명 
	 * @return  Data 현재 Data 객체 
	 * @deprecated 
	 */
	public function convertField($field_list = array()) { 
		$keys		= array_values($field_list);
		$temp_keys	= array_keys($field_list);

		$keys		= array_map('strtolower',$keys);		
		$temp_keys	= array_map('strtolower',$temp_keys);

		$this->saveIndex();

		while($this->next()) { 
			foreach ($temp_keys as $key) {
				// 이전, 이후 키 값이 같으면 필드를 바꾸지 않고 넘어간다. 
				if ($field_list[$key] === $key) continue;

				$this->set($field_list[$key], $this->get($key));
				$this->dropColumn($key);
			}
		}

		// 필드 값을 다시 구성한다. 
		foreach ($this->fields as &$key) { 
			$key = (array_key_exists ( $key, $field_list)) ? $field_list[$key] : $key;
		}

		$this->returnIndex();

		return $this;
	}

	/**
	 * 필드 데이타 삭제 
	 *
	 * 현재 데이타 배열에서 제외됩니다. 
	 *
	 * @param	string $key	바뀔 필드명 
	 * @return Data
	 */
	public function dropColumn($key) { 
		unset($this->row[$this->index][$key]);
		
		return $this;
	}

	/**
	 * 필드 삭제 
	 *
	 * 배열에서 제외됩니다. 
	 *
	 * @param	int $key	바뀔 필드 인덱스
	 * @return Data  
	 */
	public function dropField($key) { 
		unset($this->fields[$key]);
		
		return $this;
	}

	/**
	 * 레코드 삭제 
	 *
	 * 배열에서 제외됩니다. 레코드가 삭제된 다음 인덱스는 다시 정렬됩니다. 
	 * ex)  0=> 1, 1 => 2, 2 => 4,  3 => 1 , ...     에서   
	 *      $data->dropRow(0) 을 하면   0번째 인덱스가 지워지고 다시 0번부터 정렬됩니다.
	 *      
	 *      result :
	 *      0 => 2, 1 => 4, 2 => 1  
	 *  
	 *
	 * <code>
	 * $data->dropRow(0);	// 첫번째 레코드 지움 
	 * $data->dropRow(1, 2, 3);  // 여러 레코드 동시에 지움 
	 * </code>
	 *
	 * @return Data  
	 */
	public function dropRow() { 

		$index_list = func_get_args();

		foreach ($index_list as $index ) { 
			unset($this->row[$index]);
		}

		$this->setRows($this->row);
		
		return $this; 
	}

	/**
	 * 데이타 비움 
	 *
	 * @return Data 
	 */
	public function clean() { 
		$this->setRows(array());
		return $this;
	}

	/**
	 * 필드 리스트 추가
	 *
	 * @param	string $key	추가될 필드명
	 * @return Data 
	 */
	public function addField($key) {
		$this->fields[] = $this->filterKey($key);
		
		return $this; 
	}

	/**
	 * 필드 리스트 설정
	 *
	 * @param	array $arr	추가될 필드명 리스트 
	 * @return Data  
	 */
	public function setFields($arr)
	{
		$this->fields = array_map('strtolower',$arr);

		return $this;
	}

	/**
	 * 필드 리스트 얻어오기
	 *
	 * @return	array		필드 리스트 배열 
	 */
	public function getFields()
	{
		return $this->fields;
	}

	/**
	 * 기본 클래스 설정 
	 *
	 * @param	string $class	기본 클래스명
	 * @deprecated
	 */
	public function setBaseClass($class)
	{
		$this->baseClass = $class;
	}

	/**
	 * 기본 클래스 설정 
	 *
	 * @return string 클래스 이름
	 * @deprecated  
	 */
	public function getBaseClass()
	{
		return $this->baseClass;
	}

	/**
	 * 전체 row 개수 얻어오기 
	 *

	 * @return	int			row 개수 
	 */
	public function getListCount()
	{
		return $this->listCount;
	}

	/**
	 * 전체 필드 개수 얻어오기 
	 *
	 * @return	int			필드 개수 
	 */
	public function getFieldCount()
	{
		return count($this->fields);
	}

	/**
	 * column 값에 의한 배열 구하기
	 *
	 * <code>
	 * // 1. 1차원 배열 얻기   
	 *  	$data->getColumnList('필드명');    
	 * 
	 * //   필드명에 해당되는 column 값에 대한 배열을 리턴해줍니다.
	 *
	 * //   2. 연관 배열 얻기 
	 *
	 *			$data->getColumnList('필드명', 'key 필드명');    
	 *
	 *	//	array('key필드명' => '필드명', ...... ) 형태의 배열로 리턴해줍니다.
	 * </code>
	 *
	 * @param	string $field	값 필드
	 * @param	string $key		key 필드, 기본값은 ''
	 * @return	int			필드 개수 
	 */
	public function getColumnList($field, $key = '') {
		// 데이타 목록 만들기
		$this->saveIndex();

		$temp = array();

		while($this->next()) {
			if ($key) { 
				$temp[$this->get($key)] = $this->get($field);
			} else { 
				$temp[] = $this->get($field);
			}
		}

		// 인덱스 돌려주기 
		$this->returnIndex();

		return $temp;
	}

	/**
	 * 중복을 제외한 리스트 얻어오기 
	 *
	 * @return array  중복을 제거한 배열
	 */
	public function distinct($field) { 
		return array_unique($this->getColumnList($field));
	}


	/**
	 *  Data객체 내에서 검색 한 결과를 Data 객체로 리턴 
	 *
	 * <code> $new_data = $data->find('필드', '값'); </code>
	 *
	 * @param	string $field	검색할 필드
	 * @param	string|array $value	검색할 값
	 * @return	Data 			
	 *
	 */
	public function find($field, $value) { 

		if (is_array($value)) { 
			$value = "'".implode("','", $value)."'";
			$format = "in_array('{{$field}}', array($value))";
		} else { 
			$format = "'{{$field}}' == '$value'";
		}

		$data = $this->where($format);

		return $data;
	}

	/**
	 *  원하는 필드로만 데이타 리턴 
	 *
	 *  <code> $new_data = $data->column('필드1', '필드2','필드3'); </code>
	 *
	 * @param	array $field_list	분리할 필드 리스트 
	 * @return	Data 			
	 *
	 */
    public function column() { 

		// 필드 리스트 가지고 오기 
		$field_list = func_get_args();

		$this->saveIndex();

		$field_list = array_map('strtolower', $field_list);

		$field_list = (count($field_list) == 0) ? $this->getFields() : $field_list;

		$data = new Data($this->getConnector());
		$data->setFields($field_list);

		while($this->next()) { 
			$arr = array();

			foreach ($field_list as $field) { 
				$arr[$field] = $this->get($field);
			}

			$data->add($arr);
		}

		$this->returnIndex();

		return $data;
	}

	/**
	 * 상위 몇개만 Data 리턴
	 *
	 * <code>$new_data = $data->top(5);</code>
	 *
	 * @param	int $count	상위 개수 
	 * @return	Data 			
	 *
	 */
	public function top($count) { 
		return $this->page(1, $count);
	}

	/** 
	 * Page 화 된 Data 리턴 
	 *
	 * <code>$new_data = $data->page(1, 10);</code>
	 *
	 * @param	int $page		현재 페이지
	 * @param	int $page_size	페이지 제한 개수 
	 * @return	Data 			
	 *
	 */
	public function page($page = 1, $page_size = 10) { 

		$this->saveIndex();

		$data = new Data($this->getConnector());
		$data->setFields($this->getFields());

		$start = ($page - 1)*$page_size - 1;		// 시작지점은 항상 -1 

		$this->setIndex($start);
		
		for($i = 0; $this->next() && $i < $page_size; $i++) { 
			$data->add($this->gets());
		}

		$this->returnIndex();

		return $data;

	}

	/** 
	 * 정해진 개수로 Data 객체의 배열을 만듦 
	 *
	 * <code>$new_data_list = $data->split(10);</code>
	 *
	 * @param	int $count		분할될 기준 개수 , 기본 10
	 * @return	array 			
	 *
	 */
	public function split($count = 10) { 
		$page_count = $this->getPageCount($count);
		$arr = array();

		for ($i = 1; $i <= $page_count ; $i++) { 
			$arr[] = $this->page($i, $count);	
		}

		return $arr;
	}

	/**
	 * 리스트 개수를 기준으로 해서 페이지 개수를 구한다.
	 *
	 * <code>$page_count = $data->getPageCount(10);</code>
	 *
	 * @param	int $count		분할될 기준 개수 , 기본 10
	 * @return	int				페이지 개수  			
	 */
	public function getPageCount($count = 10) { 
		return ceil($this->getListCount() / $count);
	}

	/**
	 * table 태그 객체 리턴
	 *
	 * <pre>
	 *   테스트 데이타 
	 * 
	 *   NUM | NAME | AGE
	 *    1  | ZINO | 28 
	 *    2  | SU   | 20 
	 *
	 * ex)  1. $data->toDataTable();			
	 *
	 *		| NUM | NAME | AGE |
	 *		|  1  | ZINO | 28  |
	 *		|  2  | SU   | 20  |
     *
	 *		전체 데이타를 그대로 table 태그로 만듦
	 *
	 *      2. $data->toDataTable(array('num'));
	 *
	 *		| NUM | 
	 *		|  1  | 
	 *		|  2  | 
	 *
	 *		num 필드에 관해서만 table 태그로 만듦
	 *
	 *      3. $data->toDataTable(array('num'), array('num' => '일련번호'));
	 *
	 *		| 일련번호 | 
	 *		|  1       | 
	 *		|  2       | 
	 *
	 *		num 필드에 관해서만 만들고 제목을 출력을 할때 'num' 은 '일련번호' 로 바껴서 출력해서 리턴한다. 
	 *  </pre>
	 *

	 * @param	array $field_list	보여주고 싶은 필드 리스트
	 * @param	array $name_list	필드 리스트에 다른 이름 리스트
	 * @return	Tag			table Tag 객체 
	 */
	public function toDataTable($field_list = array(), $name_list = array()) 
	{
		$table = tag_("table", true);
		$table->setAttribute(
			array('border' => '1')
		);
	
		// 출력될 필드 설정 
		$field_list = (count($field_list) == 0) ? $this->fields : $field_list ;
		$hasName = (count($name_list) > 0) ? true : false;

		// 필드 목록 만들기 
		$tr = tag_("tr", true);

		foreach ($field_list as $field) {
			$th = tag_("th",true);

			if ($hasName) { 
				$th->add(isset($name_list[$field]) ? $name_list[$field] : $field);
			} else {
				$th->add($field);
			}
			$tr->add ($th);
		}

		$table->add($tr);

		// 데이타 목록 만들기
		$this->saveIndex();

		while($this->next()){  
			$tr =  tag_("tr", true);

			foreach ($field_list as $field){
				$f = tag_("td", true);
				$f->add($this->get($field));

				$tr->add($f);
			}

			$table->add($tr);
		}

		$this->returnIndex();

		return $table;
	}

	/*
	 * 테이블 출력 2번째 버전 
	 *
	 * html 태그가 아닌 text 로 출력 
	 *
	 * <pre>
	 * +--------+-------+
	 * | 필드1  | 필드2 |
	 * +--------+-------+
	 * | 값     | 값    | 
	 * +--------+-------+
	 * </pre>
	 * 
	 * 웹에서 출력할 때 [pre] 태그를 붙여주면 좋다 
	 * <code>echo "<pre>",$data->toScreen(),"</pre>";</code>
	 *
	 * 필드이름을 바꿔서 보여줄 때 
	 * 
	 * <code>echo $data->toScreen(array('field1','field2'), array('field1' => '필드1','field2' => '필드2'));</code>
	 *
	 * @param array $fields 보여주고 싶은 필드 리스트 
	 * @param array $names 필드의 다른 이름 리스트 
	 * @return string 
	 * 
	 */
	public function toScreen($fields = array(), $names = array())
	{
		$fields = array_map('strtolower', $fields);
		$names = $this->map($names);

		$field_list = (count($fields) == 0) ? $this->fields : $fields ;
		$hasName	= (count($names) > 0) ? true : false;

		// 길이 구하기 
		$field_length = array();

		foreach ($field_list as $field) { 
			if ($hasName) { 
				$field_length[$field] = strlen($names[$field]);
			} else { 
				$field_length[$field] = strlen($field);
			}
		}

		// 데이타 길이 구하기 
		$this->saveIndex();

		while($this->next()){
			foreach ($field_list as $field) {
				$value = $this->get($field);


				if (is_null($value)) { 
					$value = "[NULL]";
				} else { 

					$value = trim($value);

					if (strlen($value) > 100) { 
						$value = "[Long Text]";
					}
	
				}

				$field_length[$field] = max($field_length[$field], strlen($value));
			}
		}

		$this->returnIndex();


		// 필드 길이 
		$header_length = array();

		foreach ($field_list as $field) { 
			$header_length[$field] = str_repeat("-", $field_length[$field] + 2);
		}

		$separator = '+'.implode('+', $header_length).'+';


		// 헤더 스트링
		$header_length = array();

		foreach ($field_list as $field) { 
			if ($hasName) { 
				$header_length[$field] = " ".str_pad($names[$field], $field_length[$field])." ";
			} else { 
				$header_length[$field] = " ".str_pad($field, $field_length[$field])." ";
			}
		}

		$header_string = '|'.implode('|', $header_length).'|';


		$output = array($separator, $header_string, $separator);

//		print_p(array($separator, $header_string, $separator));


		$this->saveIndex();
		
		while($this->next()) { 
			$temp = array();
			foreach ($field_list as $field) { 
				$value = is_null($this->get($field)) ? "[NULL]" : $this->get($field);
				$value = trim($value);
				if (strlen($value) > 100) { 
					$value = "[Long Text]";
				}

				$temp[$field] = " ".str_pad($value, $field_length[$field])." ";
			}

			$output[] = '|'.implode('|', $temp).'|';
		}

		$this->returnIndex();

		$output[] = $separator;

		$str = implode(PHP_EOL, $output);

		return $str;
	}


	/*
	 * 테이블 출력 1번째 버전 
	 *
	 * table 태그로 출력
	 *
	 * <pre>
	 *      <table>
	 *          <tr><td>필드1</td><td>필드2</td></tr>
	 *          <tr><td>값1</td><td>값2</td></tr>
	 *      </table>
	 * </pre>
	 * 
	 * 필드이름을 바꿔서 보여줄 때 
	 * 
	 * <code>echo $data->toString(array('field1','field2'), array('field1' => '필드1','field2' => '필드2'));</code>
	 *
	 * @param array $fields 보여주고 싶은 필드 리스트 
	 * @param array $names 필드의 다른 이름 리스트 
	 * @return string 
	 * 
	 */
	public function toString($fields = array(), $names = array())
	{
		$obj = $this->toDataTable($fields, $names);

		return $obj->toString();
	}

	/**
	 * 화면상에 보임 , toString 메소드를 이용해서 화면출력을 한다.
	 * 
	 * <code>echo $data->display(array('field1','field2'), array('field1' => '필드1','field2' => '필드2'));</code>
	 *
	 * @param array $fields 보여주고 싶은 필드 리스트 
	 * @param array $names 필드의 다른 이름 리스트 
	 */
	public function display($fields = array(), $names = array())
	{
		echo $this->toString($fields, $names);
	}

	/**
	 * 화면상에 보임 , toScreen 메소드를 이용해서 화면출력을 한다.
	 * 
	 * <code>echo $data->screen(array('field1','field2'), array('field1' => '필드1','field2' => '필드2'));</code>
	 *
	 * @param array $fields 보여주고 싶은 필드 리스트 
	 * @param array $names 필드의 다른 이름 리스트 
	 */
	public function screen($fields = array(), $names = array())
	{
		echo $this->toScreen($fields, $names);
	}

	/**
	 * JSON 코드로 변환 
     * 
	 *  all 
	 * 
	 *  array('field' => .... , 'row' => .... )
	 *
	 *  field 
	 * 
	 *  array( .... )
	 * 
	 *  row 
	 * 
	 *  array( ... )
	 *
	 * @param string $option    all, field, data 
	 * @return string	json 문자열
	 */
	public function toJSON($option = 'all') { 
		
		$option = strtolower($option);

		if ($option == 'all') { 
			return json_encode(array('field' => $this->fields, 'row' => $this->row));
		} else if ($option == 'field') { 
			return json_encode($this->fields);
		} else if ($option == 'row') { 
			return json_encode($this->row);
		}

		return json_encode("");
	}

	/**
	 * 일정한 형식의 xml로 변환해준다. 
	 * 
	 * <pre>
	 * < data>
	 * 	< record>
	 * 		< field1></ field1>
	 * 		< field2></ field2>
	 * 		< field3_list>
	 * 			< data>....</ data>
	 * 		</ field3_list>
	 * 	 </ record>
	 * 	</ data>
	 * </pre>
	 * 
	 * @return string xml 형식 문자열 
	 */
	public function toXml()
	{
		$xml = tag_("data", true);
		$this->saveIndex();

		while($this->next()){  
			$record =  tag_("record", true);

			foreach ($this->fields as $field){

				if (PObject::isObject($this->get($field), 'Data')) { 
					$f = tag_(strtolower($field)."_list", true);					
					$str = $this->get($field)->toXml();
				} else { 
					$f = tag_(strtolower($field), true);
					$str = $this->get($field);
				}

				$f->add($str);

				$record->add($f);
			}

			$xml->add($record);
		}

		$this->returnIndex();

		return $xml->toString();
	}

	/**
	 * 현재 Data 객체에 다른 Data 객체 설정
	 *
	 * @param	Data $data	외부 Data 객체 
	 */
	public function setData(Data $data)
	{
		$this->setFields($data->getFields());
		$this->setRows($data->getRows());
		$this->init();

		return $this;
	}

	/** 
	 * 정렬 기능 추가 
	 *
	 *
	 *  <code>
	 *  $data = new Data();
	 *
	 *	$data->sort(array('field1' => SORT_DESC, 'field2' => SORT_ASC));
     *	$data->sort(array('field1' => 'desc', 'field2' => 'asc'));
     *  $data->sort('field1 desc, field2 asc');  // 구현 예정  
	 *
	 *	echo $data;
	 *  </code>
	 *
	 * @param	string|array $arr	정렬 타입 
	 * @return Data 
	 **/
	public function sort($arr) {
		
		// 문자열로 들어올 때 arr 형태로 변경한다. 
		if (is_string($arr)) { 
			$temp = array_map('trim', explode(",", $arr));
			$sort = array();
			foreach ($temp as $val) { 
				$s = preg_split("/[\s,]+/", $val);
				
				$sort[$s[0]] = $s[1];
			}
			
			$arr = $sort;
		}
		
		$arr = $this->map($arr);

		$this->saveIndex();
		
		$temp = array();
		
		// 동적 파라미터 구성
		$params = array();

		foreach ($arr as $key => $desc) { 
			// 정렬될 데이타 설정 
			${$key} = $this->getColumnList($key);		

			// 정렬 필드 설정
			$params[] = ${$key};						

			// 정렬 방식 설정
			$params[] = (strtolower($desc) == 'desc' or $desc == SORT_DESC ) ? SORT_DESC : SORT_ASC ;	
		}

		$params[] = &$this->row;						// 전체 데이타 참조 변수 


		// 데이타 정렬 시키기 완성 
		call_user_func_array('array_multisort', $params);

		$this->returnIndex();

		return $this;
	}

	/** 
	 * 합계 구하기 
	 * 
	 * @param string $field  합계 구하고자 하는 필드이름
	 * @return int|float 합계
 	 */
    public function sum($field) { 
		$arr = $this->getColumnList($field);

		return array_sum($arr);
	}

	/** 
	 * 평균 구하기
	 * 
	 * @param string $field  평균 구하고자 하는 필드이름
	 * @return int|float 평균
 	 */
	public function avg($field) { 
		$sum	= $this->sum($field);
		$count	= $this->getListCount();

		return ($count == 0) ?  0 : ($sum/$count);
	}

	/**
	 * 통계 데이타 추출 
	 *
	 * <code>
	 * // 1. 
     * echo $data->groupBy('필드1,필드2', array(array('format' => '{필드3}', 'group' => 'sum', 'name' => '필드합계')));
	 * // 2. 
     * echo $data->groupBy(array('필드1','필드2'), array(array('format' => '{필드3}', 'group' => 'sum', 'name' => '필드합계')));
	 * 
	 * </code>
	 *
	 * @return Data 
	 */
	public function groupBy($group_field, $calc_field = array()) { 
		
		// 필드 리스트가 문자열로 나왔다면 
		// 배열형태로 변환 
		if (gettype($group_field) == 'string') { 
			$group_field = a_()-> r('explode', ",", $group_field)
							   -> r('map','trim')
				               -> to;
		}

		$arr = $this->groupByInit($group_field, $calc_field); 

		// data 객체 만들기
		$group_list = array();

		foreach ($calc_field as $calc) { 
			if (gettype($calc) == 'string') { 
				$group_list[$calc] = $calc;
			} else { 
				$group_list[$calc['name']] = $calc['group'];
			}
		}


		// 계산된 값 담을 배열 
		$temp = array();
		
		// 값 계산하기 
		foreach ($arr as $value_list) { 


			if (!array_key_exists($value_list['%%key_field%%'], $temp)) { 
				$temp[$value_list['%%key_field%%']] = array();
			}

			foreach ($value_list as $key => $value) { 

				// 값 필드라면 
				if ($key != '%%key_field%%') { 

					$group = $group_list[$key];

					switch($group) { 
					case 'count' : 
						$temp[$value_list[Data::KEY_FIELD]][$key]['count'] += 1;
						break;
					case 'sum' : 
						$temp[$value_list[Data::KEY_FIELD]][$key]['sum'] += $value;
						break;
					case 'avg' : 
						$temp[$value_list[Data::KEY_FIELD]][$key]['count'] += 1;
						$temp[$value_list[Data::KEY_FIELD]][$key]['sum'] += $value;
						$temp[$value_list[Data::KEY_FIELD]][$key]['avg'] = 1;
						break;
					default : 
						$temp[$value_list[Data::KEY_FIELD]][$key]['count'] += 1;
						break;
					}
				}
			}
		}

		// 평균 구하기 
		foreach ($temp as &$data) { 
			foreach ($data as &$group_by_data) { 
				// 평균 인지 확인해서 
				if ($group_by_data['avg'] == 1) { 
					// 평균을 구하고 
					$group_by_data['avg'] = ($group_by_data['count'] == 0) ? 0 : $group_by_data['sum']/$group_by_data['count'];

					// 기존에 가지고 있던 데이타는 메모리 상에서 삭제 
					unset($group_by_data['count']);
					unset($group_by_data['sum']);
				}
			}
		}

		$field_list = array_merge($group_field, array_keys($group_list));


		// 그룹 데이타 생성 
		$group_data = data_(null, $field_list);

		foreach ($temp as $key_field => $value_data ) { 

			$key_list = $this->getGroupByDivideField($key_field);

			// group by 필드 셋팅 
			$temp_field = array_combine($group_field, $key_list);

			// 통계 데이타 필드 셋팅 
			foreach ($value_data as $field => $value) { 
				$temp_field[$field] = array_sum($value);
			}

			// 데이타 넣기 
			$group_data->add($temp_field);
		}


		return $group_data;
	}

	// 기본 데이타 
	protected function groupByInit($group_field, $calc_field) { 
		$arr = array();
		
		$this->saveIndex();

		while($this->next()) { 
			$key_field = $this->getGroupByKeyField($group_field);

			$temp = array(Data::KEY_FIELD => $key_field);
			
			$temp = array_merge($temp, $this->getGroupByField($calc_field));

			$arr[] = $temp;
		}

		$this->returnIndex();

		return $arr;
	}

	// 계산 필드 초기화하기 
	protected function initGroupByField($f) { 
		// 1. as 로 이름을 분리해서 포맷이랑 별칭을 구별한다. 
		if (gettype($f) == 'string') { 
			return array('name' => $f, 'format' => "{".$f."}", 'group' => 'count');
		}

		return $f;

	}

	/**
	 * 정해진 포맷으로 실행 
	 * 
	 * <code>echo $data->run('{a}-{b}-{c}');</code>
	 *  
	 * @param string $format 실행된 포맷 
	 * @return mixed 실행된 리턴값 
	 */
	public function run($format) { 

		// 포맷 설정 
		$format = $this->template($format);

		// 변수 대입 
		$code = "\$temp = $format;";

		// 코드 분석
		eval($code);

		return $temp;
	}

	// 필드값 계산 
	protected function getGroupByField($calc_field) { 
		$temp = array();

		foreach ($calc_field as $f) { 

		    $f = $this->initGroupByField($f);

			$temp[$f['name']] = $this->run($f['format']);
		}

		return $temp;
	}

	/**
	 * 템플릿 변환 
	 *
	 * 주어진 템플릿에 대해서 필드값 a,b,c 의 값으로 변환해서 문자열을 넘겨준다. 
	 * 
	 * {필드이름} 으로 필드 구분
	 * 
	 * <code>echo $data->template('{a}-{b}-{c}');</code>
	 * 
	 * @param string $format 변환될 문자열 
	 * @return string 
	 */
	public function template($format = '') { 
		$pattern = "/\{([^\}]+)\}/";

		preg_match_all($pattern, $format, $all);

		foreach ($all[0] as $key => $value) { 
			$format = str_replace($value, $this->get($all[1][$key]), $format);
		}

		return $format;		
	}

	// 그룹값 매칭
	protected function getGroupByKeyField($group_field) { 
		$temp = array();

		foreach ($group_field as $f) { 
			$temp[] = $this->get($f);
		}

		return implode("%%", $temp);
	}

	// %%로 구분하기 
	protected function getGroupByDivideField($key_field) { 
		return explode("%%", $key_field);		
	}

	/**
	 * 조건에 따른 Data 구하기 
	 * 
	 * @param string $format 체크될 조건 
	 * @return Data 조건에 검색된 Data객체
	 */
	public function where($format) { 

		$format = "($format)";

		$this->saveIndex();

		$data = data_($this->getConnector(), $this->getFields());

		while($this->next()) { 
			if ($this->run($format)) { 
				$data->add($this->gets());
			}
		}

		$this->returnIndex();

		return $data;
	}

	/**
	 * 속성 존재여부 확인 
	 * 
	 * @return boolean
	 */
	public function offsetExists($name) {
		return in_array(strtolower($name), $this->fields);
	}

	/**
	 * 속성 값 얻어오기 
	 * 
	 * @return mixed
	 */
	public function offsetGet($name) {
		return $this->get($name);
	}

	/**
	 * 속성 값 설정
	 */
	public function offsetSet($name, $value) {
		$this->set($name, $value);
	}

	/**
	 *  속성 값 비우기 
	 */
	public function offsetUnset($name) {
		$this->dropColumn($name);
	}



	/**
	 * date 형식 문자열 만들어주기 
	 * 
	 * <code>
	 * $date = $data->date('writeDate', 'Y-m-d');
	 * echo $date;
	 * </code>
	 * 
	 * @param string $field 필드명 
	 * @param string $format 문자열로 변환될 포맷, date() 함수 사용 
	 * @return string 
	 */
	public function date($field, $format = 'Y-m-d') {
		return date($format, strtotime($this->get($field)));
	}
	
	/**
	 * 문자열 tag 제거한 후에 쪼개기
	 * 
	 * <code>
	 * $title = $data->prefix('title', 30, "...", 'euc-kr');
	 * echo $title;
	 * </code>
	 * 
	 * @param string $field
	 * @param int $size
	 * @param string $subfix
	 * @param string $encode
	 * @return string
	 */
	public function prefix($field, $size, $subfix = "...", $encode = 'euc-kr') {
		$temp = strip_tags($this->get($field));
		
		if (strlen($temp) > $size) { 
			return mb_substr($temp, 0, $size, $encode).$subfix;	
		} else { 
			return $temp;		
		}
		
	}	
	
	/**
	 * Y, N 값에 따른 결과값 변경 
	 * 
	 * @param string $field
	 * @param string $Y
	 * @param string $N
	 * @return string
	 */
	public function yn($field, $Y = '', $N = '') {
		$value = $this->get($field); 
		
		return ($value == 'Y') ? $Y : $N;
	}
	
	/**
	 * Y,N,C 값에 대해서 문자열 리턴 
	 * 
	 * @param string $field
	 * @param string $Y
	 * @param string $N
	 * @param string $C
	 * @return string
	 */
	public function ync($field, $Y = '', $N = '', $C = '') { 
		$value = $this->get($field); 
		
		if ($value == 'Y') return  $Y;
		if ($value == 'N') return  $N;
		
		return  $C;
	}
	
	/**
	 * sprintf 함수로 포맷팅 하기 
	 * 
	 * @param string $field 변환할 필드
	 * @param string $format 변환될 포맷 , sprintf 에서 동일한 것을 사용 
	 * @return string
	 */
	public function format($field, $format) { 
		return sprintf($format, $this->get($field));
	}
	
	/**
	 * 해당 필드에 대해서 htmlentities 함수를 적용한다. 
	 * 
	 * @param string $field
	 * @param int $option
	 * @return string 
	 */
	public function html($field, $option = ENT_COMPAT, $encode = 'ISO-8859-1') { 
		return htmlspecialchars ($this->get($field), $option, $encode);	
	}

	/**
	 * field 값에 특정 문자 채우기 
	 * 
	 * @param string $field
	 * @param int $count
	 * @param int $direction
	 * @return string
	 */
	public function pad($field, $count, $pad = '*', $direction = 'right') { 
		
		
		if (in_list($direction, 'right', 'left', 'both')) { 
			$direction = constant("STR_PAD_".strtoupper($direction));
		} else { 
			$direction = STR_PAD_RIGHT;	
		}
		
		return str_pad($this->get($field), $count, $pad, $direction);
	}
	
	/**
	 * sscanf 를 사용하여 원하는 값을 바로 출력  
	 *
	 *
	 * @param string $field 
	 * @param string format 
	 * @return array 
	 */
	public function scan($field, $format) { 
		return sscanf($this->get($field), $format);
	}
	
	/**
	 * 배열 값을 바로 출력 
	 * 
	 * $arr 변수가 string 일 경우는 $subfix 와 함께 GLOBALS 변수를 체크한다. 
	 *
	 * @param string $field 
	 * @param string|array $arr
	 * @param string $subfix
	 * @return string
	 * 
	 */
	public function value($field, $arr = array(), $subfix = "_LiST") { 
		if (is_string($arr)) { 
			$arr = $GLOBALS[strtoupper($arr.$subfix)];
		}
		
		return $arr[$this->get($field)];
	}	
	
	/**
	 * 필드값 배열로 리턴
	 * 
	 * @param string $field
	 * @param string $separator
	 * @return array
	 */
	public function explode($field, $separator = "") { 
		return explode($separator, $this->get($field));
	}
}
?>